# -*- coding: UTF-8 -*-
import os
import re
import inspect
from django.core import meta
from django import templatetags
from django.conf import settings
from django.models.core import sites
from django.views.decorators.cache import cache_page
from django.core.extensions import DjangoContext as Context
from django.core.exceptions import Http404, ViewDoesNotExist
from django.utils.httpwrappers import HttpResponse, HttpResponseRedirect
from django.core import template, template_loader, defaulttags, defaultfilters, urlresolvers
try:
    from django.parts.admin import doc
except ImportError:
    doc = None

# Les méthodes dont le nom commence par ces chaînes seront exclues de la 
# documentation
MODEL_METHODS_EXCLUDE = ('_', 'add_', 'delete', 'save', 'set_')

def doc_index(request):
    if not doc:
        return missing_docutils_page(request)

    t = template_loader.get_template('doc/index')
    c = Context(request, {})
    return HttpResponse(t.render(c))

def bookmarklets(request):
    t = template_loader.get_template('doc/bookmarklets')
    c = Context(request, {
        'admin_url' : "%s://%s" % (os.environ.get('HTTPS') == 'on' and 'https' or 'http', request.META['HTTP_HOST']),
    })
    return HttpResponse(t.render(c))

def template_tag_index(request):
    if not doc:
        return missing_docutils_page(request)

    # On doit conserver une copie de sauvegarde de registered_tags juste au cas 
    # où le chargement des balises personnalisées altèrerait cette variable
    saved_tagset = template.registered_tags.copy(), template.registered_filters.copy()
    load_all_installed_template_libraries()

    # On rassemble les différentes parties de la documentation
    tags = []
    for tagname in template.registered_tags:
        title, body, metadata = doc.parse_docstring(template.registered_tags[tagname].__doc__)
        if title:
            title = doc.parse_rst(title, 'tag', 'balise:' + tagname)
        if body:
            body = doc.parse_rst(body, 'tag', 'balise:' + tagname)
        for key in metadata:
            metadata[key] = doc.parse_rst(metadata[key], 'tag', 'balise:' + tagname)
        library = template.registered_tags[tagname].__module__.split('.')[-1]
        if library == 'template_loader' or library == 'defaulttags':
            library = None
        tags.append({
            'name'    : tagname,
            'title'   : title,
            'body'    : body,
            'meta'    : metadata,
            'library' : library,
        })

    # On rétablit registered_tags
    template.registered_tags, template.registered_filters = saved_tagset

    t = template_loader.get_template('doc/template_tag_index')
    c = Context(request, {
        'tags' : tags,
    })
    return HttpResponse(t.render(c))
template_tag_index = cache_page(template_tag_index, 15*60)

def template_filter_index(request):
    if not doc:
        return missing_docutils_page(request)

    saved_tagset = template.registered_tags.copy(), template.registered_filters.copy()
    load_all_installed_template_libraries()

    filters = []
    for filtername in template.registered_filters:
        title, body, metadata = doc.parse_docstring(template.registered_filters[filtername][0].__doc__)
        if title:
            title = doc.parse_rst(title, 'filter', 'filtre:' + filtername)
        if body:
            body = doc.parse_rst(body, 'filter', 'filtre:' + filtername)
        for key in metadata:
            metadata[key] = doc.parse_rst(metadata[key], 'filter', 'filtre:' + filtername)
        metadata['AcceptsArgument'] = template.registered_filters[filtername][1]
        library = template.registered_filters[filtername][0].__module__.split('.')[-1]
        if library == 'template_loader' or library == 'defaultfilters':
            library = None
        filters.append({
            'name'    : filtername,
            'title'   : title,
            'body'    : body,
            'meta'    : metadata,
            'library' : library,
        })

    template.registered_tags, template.registered_filters = saved_tagset

    t = template_loader.get_template('doc/template_filter_index')
    c = Context(request, {
        'filters' : filters,
    })
    return HttpResponse(t.render(c))
template_filter_index = cache_page(template_filter_index, 15*60)

def view_index(request):
    if not doc:
        return missing_docutils_page(request)

    views = []
    for site_settings_module in settings.ADMIN_FOR:
        settings_mod = __import__(site_settings_module, '', '', [''])
        urlconf = __import__(settings_mod.ROOT_URLCONF, '', '', [''])
        view_functions = extract_views_from_urlpatterns(urlconf.urlpatterns)
        for (func, regex) in view_functions:
            title, body, metadata = doc.parse_docstring(func.__doc__)
            if title:
                title = doc.parse_rst(title, 'view', 'vue:' + func.__name__)
            views.append({
                'name'   : func.__name__,
                'module' : func.__module__,
                'title'  : title,
                'site_id': settings_mod.SITE_ID,
                'site'   : sites.get_object(id__exact=settings_mod.SITE_ID),
                'url'    : simplify_regex(regex),
            })
    t = template_loader.get_template('doc/view_index')
    c = Context(request, {
        'views' : views,
    })
    return HttpResponse(t.render(c))
view_index = cache_page(view_index, 15*60)

def view_detail(request, view):
    if not doc:
        return missing_docutils_page(request)

    mod, func = urlresolvers.get_mod_func(view)
    try:
        view_func = getattr(__import__(mod, '', '', ['']), func)
    except (ImportError, AttributeError):
        raise Http404
    title, body, metadata = doc.parse_docstring(view_func.__doc__)
    if title:
        title = doc.parse_rst(title, 'view', 'vue:' + view)
    if body:
        body = doc.parse_rst(body, 'view', 'vue:' + view)
    for key in metadata:
        metadata[key] = doc.parse_rst(metadata[key], 'view', 'vue:' + view)
    t = template_loader.get_template('doc/view_detail')
    c = Context(request, {
        'name'      : view,
        'summary'   : title,
        'body'      : body,
        'meta'      : metadata,
    })
    return HttpResponse(t.render(c))

def model_index(request):
    if not doc:
        return missing_docutils_page(request)

    models = []
    for app in meta.get_installed_model_modules():
        for model in app._MODELS:
            opts = model._meta
            models.append({
                'name'   : '%s.%s' % (opts.app_label, opts.module_name),
                'module' : opts.app_label,
                'class'  : opts.module_name,
            })
    t = template_loader.get_template('doc/model_index')
    c = Context(request, {
        'models' : models,
    })
    return HttpResponse(t.render(c))

def model_detail(request, model):
    if not doc:
        return missing_docutils_page(request)

    try:
        model = meta.get_app(model)
    except ImportError:
        raise Http404
    opts = model.Klass._meta

    # On rassemble les descriptions des champs
    fields = []
    for field in opts.fields:
        fields.append({
            'name'     : field.name,
            'data_type': get_readable_field_data_type(field),
            'verbose'  : field.verbose_name,
            'help'     : field.help_text,
        })
    for func_name, func in model.Klass.__dict__.items():
        if callable(func) and len(inspect.getargspec(func)[0]) == 0:
            try:
                for exclude in MODEL_METHODS_EXCLUDE:
                    if func_name.startswith(exclude):
                        raise StopIteration
            except StopIteration:
                continue
            verbose = func.__doc__
            if verbose:
                verbose = doc.parse_rst(doc.trim_docstring(verbose), 'model', 'modèle-de-données:' + opts.module_name)
            fields.append({
                'name'      : func_name,
                'data_type' : get_return_data_type(func_name),
                'verbose'   : verbose,
            })

    t = template_loader.get_template('doc/model_detail')
    c = Context(request, {
        'name'    : '%s.%s' % (opts.app_label, opts.module_name),
        'summary' : "Champs des objets \"%s\"" % opts.verbose_name,
        'fields'  : fields,
    })
    return HttpResponse(t.render(c))

#########################
# Fonctions assistantes #
#########################

def missing_docutils_page(request):
    """Affiche un message d'erreur pour les gens qui n'ont pas docutils"""
    t = template_loader.get_template('doc/missing_docutils')
    c = Context(request, {})
    return HttpResponse(t.render(c))

def load_all_installed_template_libraries():
    # On recharge les balises par défaut
    template.registered_tags.clear()
    reload(defaulttags)
    reload(template_loader) # template_loader définit les balises block/extends

    # On charge les bibliothèques de balises personnalisées pour chaque 
    # application installée
    for e in templatetags.__path__:
        libraries = [os.path.splitext(p)[0] for p in os.listdir(e) if p.endswith('.py') and p[0].isalpha()]
        for lib in libraries:
            try:
                mod = defaulttags.LoadNode.load_taglib(lib)
                reload(mod)
            except ImportError:
                pass

def get_return_data_type(func_name):
    """Passez à cette fonction un nom de fonction et elle renverra le type de données correspondant"""
    if func_name.startswith('get_'):
        if func_name.endswith('_list'):
            return 'Liste'
        elif func_name.endswith('_count'):
            return 'Entier'
    return ''

# Ce dictionnaire associe les objets *Field aux types de données correspondants;
# les types apparaissent sous forme de chaînes descriptives. Ces chaînes peuvent
# contenir des codes de formatage; ceux-ci seront remplacés par les valeurs du
# dictionnaire *Field.__dict__ avant sortie. Si le type est None, il ne sera pas
# inclus dans la sortie.
DATA_TYPE_MAPPING = {
    'AutoField'                 : 'Entier',
    'BooleanField'              : 'Booléen (soit vrai, soit faux)',
    'CharField'                 : 'Chaîne (jusqu\'à %(maxlength)s caractères)',
    'CommaSeparatedIntegerField': 'Liste d\'entiers séparés entre eux par des virgules',
    'DateField'                 : 'Date (sans l\'heure)',
    'DateTimeField'             : 'Date (avec l\'heure)',
    'EmailField'                : 'Adresse e-mail',
    'FileField'                 : 'Chemin d\'accès d\'un fichier',
    'FloatField'                : 'Nombre décimal',
    'ForeignKey'                : 'Entier',
    'ImageField'                : 'Chemin d\'accès d\'un fichier',
    'IntegerField'              : 'Entier',
    'IPAddressField'            : 'Adresse IP',
    'ManyToManyField'           : '',
    'NullBooleanField'          : 'Booléen (soit vrai, soit faux, soit rien)',
    'PhoneNumberField'          : 'Numéro de téléphone',
    'PositiveIntegerField'      : 'Entier',
    'PositiveSmallIntegerField' : 'Entier',
    'SlugField'                 : 'Chaîne (jusqu\'à 50 caractères)',
    'SmallIntegerField'         : 'Entier',
    'TextField'                 : 'Texte',
    'TimeField'                 : 'Heure',
    'URLField'                  : 'URL',
    'USStateField'              : 'État des États-Unis (deux lettres majuscules)',
    'XMLField'                  : 'Code XML',
}

def get_readable_field_data_type(field):
    # Les champs ForeignKey constituent des cas particuliers. On utilise le type 
    # du champ situé à l'autre bout de la relation.
    if field.__class__.__name__ == 'ForeignKey':
        field = field.rel.get_related_field()
    return DATA_TYPE_MAPPING[field.__class__.__name__] % field.__dict__

def extract_views_from_urlpatterns(urlpatterns, base=''):
    """
    Renvoie une liste de vues à partir d'une liste de modèles d'URL.

    Chaque objet de la liste renvoyée est un tuple à deux éléments de la forme (fonction_vue, expression_régulière)
    """
    views = []
    for p in urlpatterns:
        if hasattr(p, 'get_callback'):
            try:
                views.append((p.get_callback(), base + p.regex.pattern))
            except ViewDoesNotExist:
                continue
        elif hasattr(p, 'get_url_patterns'):
            views.extend(extract_views_from_urlpatterns(p.get_url_patterns(), base + p.regex.pattern))
        else:
            raise TypeError, "%s ne semble pas être un modèle d'URL valide" % p
    return views

# Transforme les expressions régulières des modèles d'URL en quelque chose d'un
# peu plus lisible pour un homme : transforme une expression du style
# "^(?P<slug_sport>\w+)/athletes/(?P<slug_athlete>\w+)/$" en
# "<slug_sport>/athletes/<slug_athlete>/"

named_group_matcher = re.compile(r'\(\?P(<\w+>).+?\)')

def simplify_regex(pattern):
    pattern = named_group_matcher.sub(lambda m: m.group(1), pattern)
    pattern = pattern.replace('^', '').replace('$', '').replace('?', '').replace('//', '/')
    if not pattern.startswith('/'):
        pattern = '/' + pattern
    return pattern